import numpy as np
from pprint import pprint
import tensorflow as tf

# first we will generate two constant numbers
num1 = tf.constant([2])
num2 = tf.constant([3])

# we will add them together - it is important that the operation is not executed here,
# only the graph will be created
result = num1 + num2

# we will create a session to execute certain nodes of the graph -the list of executed notes are in sess.run
with tf.Session() as sess:
    r = sess.run(result)
    print(r)
    print(type(r))

    result_list = sess.run([result, num1, num2])
    print(result_list)

# now we will not create graph with constants but with placeholders,
# placeholders are pipes in which we can feed data to the graph
num1 = tf.placeholder(tf.int32, [])  # type and size of the placeholder
num2 = tf.placeholder(tf.int32, [])

result = tf.add(num1, num2)

with tf.Session() as sess:
    r = sess.run(result, feed_dict={num1: 3, num2: 2})  # we feed data to the placeholder
    print(r)
    r = sess.run(result, feed_dict={num1: 15, num2: 40})
    print(r)

num1 = tf.placeholder(tf.float32)
# this is the third type we will encounter today: this is a variable
# tensorflow will automatically optimize variables to minimize loss
num2 = tf.Variable([1.0], name="variable_to_optimize", trainable=True)

expected_output = tf.placeholder(tf.float32)

result = tf.add(num1, num2)

with tf.name_scope('loss'):
    dif = tf.subtract(expected_output, result)
    # we can try out different types of loss functions
    # loss = tf.nn.l2_loss(dif)
    loss = tf.square(dif)
    # loss = tf.reduce_mean(tf.square(dif))
    # loss = tf.reduce_mean(tf.abs(dif))
    # why squared and abs diff

learning_rate = 0.1  # initial step of the optimization algorithm
with tf.name_scope('optimizer'):
    # optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss)
    optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)

init = tf.global_variables_initializer()

with tf.Session() as sess:
    with tf.device("/gpu:1"):
        sess.run(init)
        for i in range(100):
            _, r, n, l = sess.run([optimizer, result, num2, loss], feed_dict={num1: 3, expected_output: 5})
            print("The number to be optimized is: " + str(n) + "  The result is: " + str(
                r) + "   The difference is: " + str(l))

tf.reset_default_graph()  # we will delete previous elements of the graph

# we have no placeholders just a simple variable (x)
num1 = tf.Variable([1.0], name="variable_to_optimize", trainable=True)

# lets solve an equation with gradient descent
# You can define your own equation, I will solve this:
#  x+x^7+sin(x)-26*x=x^5+3

# left side
part1 = tf.pow(num1, 7)
part2 = tf.sin(num1)
part3 = tf.multiply(26.0, num1)
left_side = tf.add(num1, tf.add(part1, tf.add(part2, -part3)))

# right side
right_side = tf.add(tf.pow(num1, 5), 3)

with tf.name_scope('loss'):
    diff = tf.subtract(left_side, right_side)
    # loss = tf.square(diff)
    loss = tf.abs(diff)

with tf.name_scope('optimizer'):
    # optimizer = tf.train.GradientDescentOptimizer(0.01).minimize(loss)
    optimizer = tf.train.AdamOptimizer(0.1).minimize(loss)

init = tf.global_variables_initializer()

with tf.Session() as sess:
    sess.run(init)
    for i in range(1000):
        _, n, l = sess.run([optimizer, num1, loss], feed_dict={})
        print("X: " + str(n) + " The difference is: " + str(l))

import matplotlib.pyplot as plt


def simple_linear_regression():
    tf.reset_default_graph()
    # Definition of training data:
    train_x = [3.3, 4.4, 5.5, 6.71, 6.93, 4.168, 9.779, 6.182, 7.59, 2.167, 7.042, 10.791, 5.313, 7.997, 5.654, 9.27,
               3.1, 2.0, 2.45, 5.0]
    train_y = [1.7, 2.76, 2.09, 3.19, 1.694, 1.573, 3.366, 2.596, 2.53, 1.221, 2.827, 3.465, 1.65, 2.904, 2.42, 2.94,
               1.3, 3.0, 4.2, 4.8]

    # Declaration of placeholders
    x = tf.placeholder(tf.float32)
    y = tf.placeholder(tf.float32)

    # Definition of variables that will contain the weight vector and bias vector that are related to
    # the given linear regression problem
    w = tf.Variable([1.0], name='weight')
    b = tf.Variable([1.0], name='bias')

    pred = tf.add(tf.multiply(w, x), b)  # build the comp graph
    loss = tf.square(tf.subtract(y, pred))  # define the loss function like in the previous example
    optimizer = tf.train.AdamOptimizer(1e-4).minimize(loss)  # define the training step like priviously

    init = tf.global_variables_initializer()

    with tf.Session() as sess:
        sess.run(init)

        # Let's traint it!
        for epoch in range(1000):
            # Execute a training per each point of the trining set:
            for i in range(len(train_x)):
                _ = sess.run([optimizer], feed_dict={x: train_x[i], y: train_y[i]})
        w_opt = sess.run(w)[0]
        b_opt = sess.run(b)[0]
        print("W=" + str(w_opt) + " b=" + str(b_opt))
        fig, ax = plt.subplots()

        prediction = []
        # let's calculate our predictions
        for i in range(len(train_x)):
            prediction.append(w_opt * train_x[i] + b_opt)

        # Display the result
        ax.plot(train_x, train_y, 'ro')
        ax.plot(train_x, prediction, 'bo')
        plt.show()


simple_linear_regression()
